﻿using DeepEarth.Toolkit.Common;
using System.Collections.Generic;
using DeepEarth.Toolkit.Controls.Automation;
using System.Windows.Threading;
using System;
using System.Linq;
using System.Collections.ObjectModel;
using DeepEarth.Map.Core;
using DeepEarth.Core;
using System.Windows;
using System.ComponentModel;
using System.Windows.Media.Animation;
using System.Threading;
namespace DeepEarth.Toolkit.Controls
{
    public class AutomationControl : MapInteractionControl
    {

        public AutomationView ViewModel
        {
            get { return (AutomationView)GetValue(ViewModelProperty); }
            set { SetValue(ViewModelProperty, value); }
        }

        public static readonly DependencyProperty ViewModelProperty =
            DependencyProperty.Register("ViewModel", typeof(AutomationView), typeof(AutomationControl), new PropertyMetadata(OnViewModelChangedHander));
        
        static void OnViewModelChangedHander(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            AutomationControl self = (AutomationControl)sender;

            AutomationView oldValue = args.OldValue as AutomationView;

            if (oldValue != null)
            {
                oldValue.PropertyChanged -= self.OnViewModelPropertyChanged;
            }

            self.Waypoints = self.ViewModel.Waypoints.ToList();
            self.Loop = self.ViewModel.Loop;
            self.State = self.ViewModel.State;

            self.ViewModel.PropertyChanged += self.OnViewModelPropertyChanged;
        }
        
        void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Waypoints")
            {
                this.Waypoints = ViewModel.Waypoints.ToList();
            }
            else if (e.PropertyName == "State")
            {
                this.State = ViewModel.State;
            }
            else if (e.PropertyName == "Loop")
            {
                this.Loop = ViewModel.Loop;
            }
        }

        DispatcherTimer flyToTimeoutTimer;
        TimeSpan flyToTimeout = TimeSpan.FromSeconds(10);
        int monitoredFlyToStep = 0;

        public TimeSpan FlyToTimeout
        {
            get { return flyToTimeout; }
            set
            {
                FlyToTimeout = value;
            }
        }

        int defaultAccuracy = 6;

        DispatcherTimer timer;
        Waypoint currentWaypoint;

        List<Waypoint> Waypoints { get; set; }

        int currentStep = 0;

        int CurrentStep
        {
            get
            {
                return currentStep;
            }
            set
            {
                currentStep = value;

                if (value != 0)
                {
                    Execute(value);
                }
            }
        }

        AutomationState state = AutomationState.Watch;

        public AutomationState State
        {
            get
            {
                return state;
            }
            set
            {
                if (state != value)
                {
                    state = value;

                    // Inequality check above ensures that we can't be started twice.
                    switch (value)
                    {
                        case AutomationState.Run :
                            // Ensure that we're operating on the correct thread
                            Dispatcher.BeginInvoke(() => BeginAutomation());
                            break;
                        
                        case AutomationState.Watch :
                            CurrentStep = 0;
                            goto case AutomationState.Pause;

                        case AutomationState.Pause :
                            Dispatcher.BeginInvoke(() => EndAutomation());
                            break;
                    }

                    OnStateChange();
                }
            }
        }

        bool loop = true;
        public bool Loop
        {
            get { return loop; }
            set { loop = value; }
        }

        bool fullScreen = true;
        public bool FullScreen
        {
            get { return fullScreen; }
            set { fullScreen = value; }
        }

        bool canInterrupt = false;
        public bool CanInterrupt
        {
            get { return canInterrupt; }
            set { canInterrupt = value; }
        }


        public event Action StateChanged;

        public AutomationControl()
        {
            flyToTimeoutTimer = new DispatcherTimer();
            flyToTimeoutTimer.Tick += (o, e) =>
                {
                    flyToTimeoutTimer.Stop();

                    if (CurrentStep == monitoredFlyToStep)
                    {
                        CurrentStep++;
                    }
                };

            timer = new DispatcherTimer();
            timer.Tick += (o, e) =>
                {
                    timer.Stop();
                    CurrentStep++;
                };

            this.Waypoints = new List<Waypoint>();
            this.CurrentStep = 0;
        }

        void OnStateChange()
        {
            if (StateChanged != null)
            {
                StateChanged();
            }

            HandleStateChanged();
        }

        protected virtual void HandleStateChanged()
        {
        }

        public void Insert(int index, Waypoint waypoint)
        {
            lock (Waypoints)
            {
                Waypoints.Insert(index, waypoint);
            }
        }

        public void Add(Waypoint waypoint)
        {
            lock (Waypoints)
            {
                Waypoints.Add(waypoint);
            }

            if (State == AutomationState.Watch)
            {
                State = AutomationState.Run;
            }

            HandleWaypointsChanged();
        }

        public void AddRange(IEnumerable<Waypoint> waypoints)
        {
            lock (Waypoints)
            {
                Waypoints.AddRange(waypoints);
            }

            if (State == AutomationState.Watch)
            {
                State = AutomationState.Run;
            }

            HandleWaypointsChanged();
        }

        public void Reset()
        {
            Waypoints.Clear();
            State = AutomationState.Watch;
            CurrentStep = 0;

            HandleWaypointsChanged();
        }

        protected virtual void HandleWaypointsChanged()
        {
        }

        public void BeginAutomation()
        {
            if (FullScreen)
            {
            // Can't do this programmatically typically. May work in desktop mode.
                System.Windows.Application.Current.Host.Content.IsFullScreen = true;
            }

            if (CanInterrupt)
                Map.View.PropertyChanged += View_PropertyChanged;

            if (CurrentStep == 0)
            {
                CurrentStep = 1;
            }
            else
            {
                Execute(CurrentStep);
            }
        }

        // Interrupt the workflow if the user interacts with the map while the workflow is running
        void View_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            FlyTo flyTo = currentWaypoint as FlyTo;

            // FlyTo is null, so map was manipulated by the user - pause the workflow!
            if (flyTo == null)
            {
                this.State = AutomationState.Pause;
                return;
            }
            switch (e.PropertyName)
            {
                case MapView.CENTER :
                    if (flyTo.Location != null && flyTo.Location != Map.View.Center)
                    {
                        this.State = AutomationState.Pause;
                    }
                    break;

                //case MapView.VIEWPORT_WIDTH :
                //    if (flyTo.ViewportWidth != 0 && flyTo.ViewportWidth != Map.View.ViewportWidth)
                //    {
                //        this.State = AutomationState.Pause;
                //    }
                //    break;
            }
        }


        void Execute(int stepNumber)
        {
            if (!useCustomTransitions)
                flyToTimeoutTimer.Stop();

            if (Waypoints.Count >= stepNumber)
            {
                currentWaypoint = Waypoints[stepNumber - 1];

                if (currentWaypoint is FlyTo)
                {
                    FlyTo flyTo = currentWaypoint as FlyTo;

                    if (InViscinity(defaultAccuracy, flyTo))
                    {
                        CurrentStep++;
                    }
                    else
                    {                   
                        // In case something goes wrong, set up a timeoutTimer
                        if (!useCustomTransitions)
                        {
                            if (FlyToTimeout.TotalMilliseconds > 0 && !CanInterrupt)
                            {
                                flyToTimeoutTimer.Interval = FlyToTimeout;
                                monitoredFlyToStep = CurrentStep;
                                flyToTimeoutTimer.Start();
                            }

                            Location newLocation = flyTo.Location ?? Map.View.Center;
                            double viewportWidth = flyTo.ViewportWidth != 0 ? flyTo.ViewportWidth : Map.View.ViewportWidth;

                            Map.ViewChangeEnd += Map_ViewChangeEnd;
                            Map.View.SetLocation(newLocation, viewportWidth);
                        }
                        else
                        {
                            ExecuteCustom();
                        }
                    }
                }
                else
                {
                    Wait wait = currentWaypoint as Wait;
                    timer.Interval = wait.Duration;                    
                    timer.Start();
                }

            }
            else if (Loop && Waypoints.Count > 1)
            {
                CurrentStep = 1;
            }
            else
            {
                EndAutomation();
            }
        }


        void Map_ViewChangeEnd(object sender, System.EventArgs e)
        {
            FlyTo flyTo = currentWaypoint as FlyTo;

            if (flyTo == null || (InViscinity(defaultAccuracy, flyTo)))
            {
                    Map.ViewChangeEnd -= Map_ViewChangeEnd;
                    CurrentStep++;
            }
        }

        // The map may never move to the exact location and viewport width specified (too many significant digits)
        // This checks if we're close enough.
        private bool InViscinity(int accuracy, FlyTo flyTo)
        {
            return                 
                (
                    flyTo.Location == null ||
                    Map.CurrentCenter.Within(flyTo.Location, accuracy) 
                )
                    && 
                (
                    flyTo.ViewportWidth == 0 ||
                    Math.Round(Map.CurrentViewportLogicalWidth, accuracy) == Math.Round(flyTo.ViewportWidth, accuracy)
                );
        }

        void EndAutomation()
        {
            Map.ViewChangeEnd -= Map_ViewChangeEnd;
            Map.View.PropertyChanged -= View_PropertyChanged;
            timer.Stop();

            if (State == AutomationState.Run)
            {
                State = AutomationState.Watch;
            }
        }

        #region custom automation

        bool useCustomTransitions = true;

        double frameRate = 30;
        double FrameRate
        {
            get { return frameRate; }
        }

        Storyboard automaticStoryboard;
        Transition activeTransition;

        public static readonly DependencyProperty CurrentFrameProperty = DependencyProperty.Register("CurrentFrame", typeof(double), typeof(AutomationControl), new PropertyMetadata(CurrentFrameChanged));
        public double CurrentFrame
        {
            get { return (double)GetValue(CurrentFrameProperty); }
            set { SetValue(CurrentFrameProperty, value); }
        }

        static void CurrentFrameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var playback = (AutomationControl)sender;
            playback.UpdateMapView();
        }

        AnimationLevel originalAnimationLevel;

        void ExecuteCustom()
        {
            originalAnimationLevel = Map.AnimationLevel;
            Map.AnimationLevel = AnimationLevel.None;

            FlyTo current = (FlyTo)currentWaypoint;

            if (current.Duration.TotalMilliseconds <= 0)
                current.Duration = TimeSpan.FromSeconds(1);

            activeTransition = new Transition(new MapViewLite(Map.View), new MapViewLite() { Center = current.Location, ViewportWidth = current.ViewportWidth }, current.Duration, FrameRate);

            automaticStoryboard = new Storyboard
            {
                Duration = current.Duration
            };

            var anim = new DoubleAnimation
            {
                Duration = current.Duration,
                From = 0,
                To = activeTransition.TotalFrames
            };

            anim.EasingFunction = new SineEase() { EasingMode = EasingMode.EaseInOut };

            Storyboard.SetTarget(anim, this);
            Storyboard.SetTargetProperty(anim, new PropertyPath(CurrentFrameProperty));
            automaticStoryboard.Children.Add(anim);
            automaticStoryboard.Completed += (o, e) => ThreadPool.QueueUserWorkItem(obj =>
            {
                Dispatcher.BeginInvoke(Stop);
            });
            automaticStoryboard.Begin();

        }

        public void Stop()
        {
            Map.AnimationLevel = originalAnimationLevel;
            if (automaticStoryboard != null)
            {
                automaticStoryboard.Pause();
                automaticStoryboard = null;
            }

            CurrentStep++;
        }

        /// <summary>
        /// Updates the current map view according to the current frame
        /// </summary>
        void UpdateMapView()
        {
            // Get current transition
            var proportion = CurrentFrame / activeTransition.TotalFrames;

            var targetView = activeTransition.GetView(proportion);
            this.Map.View.SetLocation(targetView.Center, targetView.ViewportWidth);
        }


        #endregion

    }
}
